# -*- coding:utf-8 -*-
"""
@author: ksf

@since: 2020-04-25 17:44
"""
import sys
import os
from collections import defaultdict
from importlib import import_module
from datetime import datetime
from typing import Dict, List, Optional, Callable


from vcat.interface import (
    BaseGateway,
    AbstractApp,
    BaseHistoryFeeder
)
from vcat.core.event_bus import (
    event_bus_left,
    event_bus_right,
    AsLeftHandler,
    EventBus
)
from vcat.core.event import Event, EventType
from vcat.core.constant import (
    HandlerLevel,
    RunningMod,
    DataLevel,
    WhenHandlerError,
    HandlerCategory
)
from vcat.utils.api_util import as_gateway
from vcat.utils.logger import get_logger
from vcat.config import conf, ROOT_PATH
from vcat.core.template import BaseTemplate
from vcat.core.obj import (
    BarData,
    BarGenArrayParams,
    TickData,
    OrderRequest,
    OrderData,
    TradeData,
    ContractData,
    PositionData,
    AccountData
)
from vcat.core.guard_left import GuardLeft, HistoryProvider
from vcat.core.guard_right import GuardRight
from vcat.utils.api_store import ApiStore


class MainEngine:

    def __init__(self, config=None, mod=RunningMod.REAL, use_day_bar=True):
        self.mod = mod
        self.use_day_bar = use_day_bar
        self.conf = config or conf
        self.logger = get_logger(log_type='system', level=self.conf.get_config()['log_level'])
        self.event_bus_left: EventBus = event_bus_left
        self.event_bus_right: EventBus = event_bus_right
        self.guard_right = GuardRight(self)
        self.guard_left = GuardLeft(self)
        self.portfolio_manager = None

        self.broker_instance: Dict[str, BaseGateway] = {}
        self.history_feeder_classes: Dict[str, BaseHistoryFeeder] = {}
        self.history_feeder_instance: Dict[str, BaseHistoryFeeder] = {}
        self.strategy_classes: Dict[str, Optional[BaseTemplate]] = {}
        self.strategy_instance: Dict[str, BaseTemplate] = {}
        self.app_instance: List[AbstractApp] = []
        self.metrics = None

        self._contract: Dict[str, ContractData] = {}
        # symbol -> level -> window -> BarGenerator
        self.contract_strategy_map: Dict[str, List[BaseTemplate]] = defaultdict(list)
        self._order_id_strategy_ins: Dict[str, str] = {}
        self._orders: Dict[str, OrderData] = {}
        self._positions: Dict[str, PositionData] = {}
        self._trades: Dict[str, PositionData] = {}
        self._ticks: Dict[str, TickData] = {}
        self._bars: Dict[str, BarData] = {}
        self._account: Dict[str, AccountData] = {}
        self._datas: Dict[str, List[object]] = defaultdict(list)
        self.inject_method()
        self.initialize()
        self.register()
        self.set_up()

    def initialize(self):
        self.load_apps()
        self.load_brokers()
        self.load_strategies()

    def inject_method(self):
        # 给其他模块注入必要函数
        self.guard_right.send_order = self.send_order
        BaseTemplate.send_order = self.guard_right.check_send_order
        BaseTemplate.get_history = self.guard_left.get_history
        BaseTemplate.subscribe = self.subscribe
        BaseTemplate.add_bar_generator = self.guard_left.add_bar_generator
        BaseTemplate.get_history_bar = self.guard_left.get_history_bar
        ApiStore.as_left_handler = self.guard_left.event_bus.as_handler
        ApiStore.get_tick = self.get_tick
        ApiStore.get_position = self.get_position
        ApiStore.get_all_positions = self.get_all_positions

    def load_brokers(self):
        self.write_log('加载broker通道...')
        brokers_names = self.conf.get_config()['gateway']
        self.write_log(brokers_names)
        for name in brokers_names:
            broker_mod = self._import_broker(name)
            BrokerClass = broker_mod.Gateway
            self.add_broker_instance(BrokerClass)

    def add_broker_instance(self, broker_class: BaseGateway):
        ins = broker_class(self)
        as_gateway(ins)
        self.broker_instance[broker_class.gateway_name] = ins

    def load_strategies(self):
        self.write_log('加载策略中。。。')
        self._load_strategies_from_path(os.path.join(ROOT_PATH, 'strategies'))
        custom_path = os.path.join(self.conf.CUSTOM_DIR, 'strategies')
        self._load_strategies_from_path(custom_path)

    def load_apps(self):
        self.write_log('加载插件。。。')
        app_names = self.conf.get_config()['apps']
        for app_name in app_names:
            mod = self._import_app(app_name)
            app_instance = mod.App(self)
            self.app_instance.append(app_instance)

    def register(self):
        ApiStore.as_left_handler(category=HandlerCategory.EVENT,event_type=EventType.LOG,
                                 level=HandlerLevel.ON_MAIN_HANDLER)(self.on_log)
        ApiStore.as_left_handler(category=HandlerCategory.EVENT, event_type=EventType.ACCOUNT,
                                 level=HandlerLevel.ON_MAIN_HANDLER)(self.on_account)
        ApiStore.as_left_handler(category=HandlerCategory.EVENT, event_type=EventType.CONTRACT,
                                 level=HandlerLevel.ON_MAIN_HANDLER)(self.on_contract)
        ApiStore.as_left_handler(category=HandlerCategory.EVENT, event_type=EventType.BAR,
                                 level=HandlerLevel.ON_MAIN_HANDLER)(self.on_bar)
        ApiStore.as_left_handler(category=HandlerCategory.EVENT, event_type=EventType.TICK,
                                 level=HandlerLevel.ON_MAIN_HANDLER)(self.on_tick)
        ApiStore.as_left_handler(category=HandlerCategory.EVENT, event_type=EventType.ORDER,
                                 level=HandlerLevel.AFTER_MAIN_HANDLER)(self.on_order)
        ApiStore.as_left_handler(category=HandlerCategory.EVENT, event_type=EventType.TRADE,
                                 level=HandlerLevel.ON_MAIN_HANDLER)(self.on_trade)

        ApiStore.as_left_handler(category=HandlerCategory.EVENT, event_type=EventType.POSITION,
                                 level=HandlerLevel.ON_MAIN_HANDLER)(self.on_position)

        ApiStore.as_left_handler(category=HandlerCategory.EVENT,event_type=EventType.QUIT,
                                 level=HandlerLevel.ON_MAIN_HANDLER)(self.on_quit)

    def _import_broker(self, name):
        try:
            self.write_log(f'正在尝试加载broker : {name}')
            broker = import_module(f'lucky_cat.gateway.{name}')
        except ImportError:
            self.write_log(f'broker {name} 非自带')
            try:
                broker = import_module(name)
            except ImportError:
                raise
        return broker

    def _import_app(self, name):
        try:
            self.write_log(f'尝试从标准目录加载app : {name} ')
            app = import_module(f'tinigine.apps.{name}')
        except ImportError:
            self.write_log(f'标准目录没有找到app {name}, 尝试从家目录寻找')
            try:
                app = import_module(name)
            except ImportError:
                raise ImportError(f'app {name} 不存在，请检查')
        return app

    def _load_strategies_from_path(self, path):
        self.write_log(f'加载策略，从目录 {path}')
        for ps, ds, fs in os.walk(path):
            abs_path = os.path.abspath(ps)
            sys.path.insert(0, abs_path)
            for f in fs:
                try:
                    name, sfx = str(f).rsplit('.', 1)
                except TypeError:
                    raise
                if sfx in ('py', 'pyd'):
                    mod = import_module(name)
                    self._get_strategies_from_mod(mod)
            sys.path.pop(0)

    def _get_strategies_from_mod(self, mod):
        """
        从给定module中提取策略模板类
        :param mod:
        :return:
        """
        for item_name in dir(mod):
            if item_name.startswith('__'):
                continue
            item = getattr(mod, item_name)
            if issubclass(item, BaseTemplate) and (item is not BaseTemplate):
                self.add_strategy_class(item)
                self.write_log(f'从 {mod.__file__} 中加载了 {item.__name__}')

    def get_strategy_cls_by_name(self, name):
        return self.strategy_classes.get(name)

    def get_strategy_ins_by_name(self, name) -> BaseTemplate:
        return self.strategy_instance.get(name)

    def get_strategy_ins_by_cid(self, contract_id) -> List[BaseTemplate]:
        return self.contract_strategy_map.get(contract_id)

    def get_strategy_ins_by_oid(self, order_id) -> BaseTemplate:
        strategy_name = self._order_id_strategy_ins.get(order_id)
        if strategy_name is None:
            return None
        return self.get_strategy_ins_by_name(strategy_name)

    def connect_gateway(self, broker_name: str, params: dict):
        """
        登录broker
        :param broker_name:
        :param params:
        :return:
        """
        b_ins = self.get_broker(broker_name)
        if b_ins is None:
            self.event_bus_left.put_log(f'交易通道 <{broker_name}> 不存在')
            return
        b_ins.connect(params)
        return True

    def add_history_feeder_class(self, hf: BaseHistoryFeeder):
        self.history_feeder_classes[hf.__name__] = hf

    def get_history_feeder_class(self, hf_name: str):
        return self.history_feeder_classes.get(hf_name)

    def add_history_feeder_instance(self, hf_name: str, params):
        hf_class = self.get_history_feeder_class(hf_name)
        if hf_class is None:
            self.write_log(f'找不到名为 <{hf_name}> 历史行情提供者')
            return False
        hf_ins = hf_class()
        if hf_ins.login(params):
            self.history_feeder_instance[hf_name] = hf_ins
            return True
        return False

    def get_all_strategy_class(self):
        return list(self.strategy_classes.values())

    def add_strategy_class(self, strategy_class: BaseTemplate):
        if strategy_class.__name__ not in self.strategy_classes:
            self.strategy_classes[strategy_class.__name__] = strategy_class

    def add_strategy_instance(self,
                              strategy_ins_name: str,
                              strategy_cls_name: str,
                              cid_list: List[str],
                              start_dt: datetime,
                              end_dt: datetime,
                              params: dict,
                              ):
        """
        添加等待运行的策略实例，添加后需要执行 init、start才会真正执行
        :param strategy_ins_name: 策略名字，为空则系统自动生成
        :param strategy_cls_name: 策略模板类名字
        :param cid_list: 标的代码列表， 600000.SH.STK
        :param start_dt:
        :param end_dt:
        :param params:策略需要调整的参数，在策略实例里可以用self.key取到
        :return: strategy_name, 可能是自动生成的
        """

        strategy_class_obj: Callable = self.get_strategy_cls_by_name(strategy_cls_name)
        if strategy_class_obj is None:
            self.write_log(f'策略类 <{strategy_cls_name}> 不存在')
            return
        elif not issubclass(strategy_class_obj, BaseTemplate):
            self.write_log(f'策略类 <{strategy_cls_name}> 不是标准策略基类的子类')
            return
        for cid in cid_list:
            if cid not in self._contract:
                self.write_log(f'[ error ] 标的<{cid}> 不存在，请检查！！！')
                return
        if strategy_ins_name is None:
            strategy_ins_name = self.generate_ins_name(strategy_cls_name,
                                                       start_dt=start_dt,
                                                       end_dt=end_dt,
                                                       params=params)
        if strategy_ins_name not in self.strategy_instance:
            strategy_instance = strategy_class_obj(
                name=strategy_ins_name,
                contract_ids=cid_list,
                start_dt=start_dt,
                end_dt=end_dt,
                mod=self.mod,
                params=params
            )
            self.strategy_instance[strategy_ins_name] = strategy_instance
            self.add_symbol_strategy_map(strategy_instance)

        return strategy_ins_name

    def add_symbol_strategy_map(self, strategy_instance: BaseTemplate):
        """
        添加行情-策略实例映射
        :param strategy_instance: 策略实例
        :return: None
        """
        for cid in strategy_instance.contract_ids:
            for p in strategy_instance.params:
                key = f'{cid}.{p.interval_level_key}'
                self.contract_strategy_map[key].append(strategy_instance)

    def init_strategy(self, strategy_name: str):
        strategy_ins = self.get_strategy_ins_by_name(strategy_name)
        if strategy_ins is None:
            self.write_log(f'策略实例 <{strategy_name}> 不存在')
            return

        tmp_guard = HistoryProvider(self,
                                    bar_callback=strategy_ins.inner_on_init_bar,
                                    tick_callback=strategy_ins.inner_on_init_tick,
                                    use_to_init=True)
        if strategy_ins.mod == RunningMod.REAL:
            self.subscribe(strategy_ins.contract_ids)

        for params in strategy_ins.gen_params_list:
            tmp_guard.add_bar_generator(b_params=params, contract_ids=list(strategy_ins.contract_ids))
        size_dict = self.get_init_bar_count(gen_params_list=strategy_ins.gen_params_list,
                                            use_day_bar=self.use_day_bar)

        count_sorted = sorted([(k, v) for k, v in size_dict.items()], key=lambda x: x[0].value, reverse=True)
        for level, size in count_sorted:
            for bar in tmp_guard.get_history_bar(contract_ids=strategy_ins.contract_ids,
                                                 start_dt=None,
                                                 end_dt=strategy_ins.start_dt,
                                                 count=size,
                                                 freq=level):
                tmp_guard.bar_bc(bar)
        strategy_ins.inner_on_init_bar(None)

    def start_backtest(self, strategy_ins_name):
        if self.mod == RunningMod.REAL:
            self.write_log('!!! 实盘模式不允许回测, 回测请另起一个进程！！！')
            return
        strategy_ins = self.get_strategy_ins_by_name(strategy_ins_name)
        if strategy_ins is None:
            self.write_log(f'策略实例 <{strategy_ins}> 不存在')
            return

        strategy_ins.start()
        self.guard_left.feed_events(
            contract_ids=strategy_ins.contract_ids,
            start_dt=strategy_ins.start_dt,
            end_dt=strategy_ins.end_dt,
            freq=strategy_ins.level
        )

    def start_strategy(self, strategy_name: str):
        if self.mod == RunningMod.BACKTEST:
            self.write_log('!!!回测模式不允许跑实盘，请另起线程.！！！')
            return
        strategy_ins = self.get_strategy_ins_by_name(strategy_name)
        if strategy_ins is None:
            self.write_log(f'策略实例 <{strategy_name}> 不存在')
            return
        strategy_ins.start()

    @staticmethod
    def get_init_bar_count(gen_params_list: List[BarGenArrayParams], use_day_bar=True):
        """
        计算初始化需要使用的 minute_bar、day_bar
        :param gen_params_list:
        :param use_day_bar: 是否使用day_bar初始化, 默认使用
        :return:
        """
        size_dict = {}
        for x in gen_params_list:
            old_size = size_dict.get(x.bar_level, 0)
            this_size = x.arr_size * x.bar_interval
            if x.bar_level == DataLevel.tick:
                continue
            elif x.bar_level == DataLevel.hour:
                this_size *= 60
            elif x.bar_level == DataLevel.day:
                if not use_day_bar:
                    # TODO 一天的交易时间不一定是 60 * 4 个minute_bar
                    this_size *= 60 * 4

            if this_size > old_size:
                size_dict[x.bar_level] = this_size
        return size_dict

    def generate_ins_name(self,
                          strategy_cls_name,
                          start_dt,
                          end_dt,
                          params: dict):
        """生成name"""
        dt1 = start_dt.strftime("%Y%m%d%H%M%S")
        dt2 = end_dt.strftime("%Y%m%d%H%M%S")
        stp = ','.join((f'{k}={v}' for k, v in params.items()))
        sta = f'{strategy_cls_name}-{dt1}-{dt2}-{stp}'
        if self.mod == RunningMod.BACKTEST:
            return 'bt-' + sta
        else:
            return 'rl-' + sta

    def get_contract(self, contract_id):
        return self._contract.get(contract_id)

    def subscribe(self, contract_ids: List[str]):
        self.guard_left.subscribe(contract_ids=contract_ids)

    def on_account(self, event: Event):
        account: AccountData = event.data
        self._account[account.id_] = account

    def on_tick(self, event: Event):
        tick: TickData = event.data
        self._ticks[tick.id_] = tick
        self._datas[EventType.TICK.name].append(tick)
        instance_list = self.get_strategy_ins_by_cid(tick.id_)
        if not instance_list:
            return
        for ins in instance_list:
            ins.on_tick(tick)

    def on_bar(self, event: Event):
        bar: BarData = event.data
        self._bars[bar.id_] = bar
        self._datas[EventType.BAR.name].append(bar)
        instance_list = self.contract_strategy_map.get(bar.id_)
        if not instance_list:
            return
        for ins in instance_list:
            ins.inner_on_bar(bar)

    def on_log(self, event: Event):
        data = event.data
        self._datas[EventType.LOG.name].append(data)

    def on_contract(self, event: Event):
        contract: ContractData = event.data
        self._contract[contract.id_] = contract

    def on_order(self, event: Event):
        order: OrderData = event.data
        old_order = self.get_order(order.t_order_id)
        if old_order:
            old_order.status = order.status
            old_order.traded = order.traded
        else:
            self._orders[order.t_order_id] = order
            self._datas[EventType.ORDER.name].append(order)

    def on_trade(self, event: Event):
        trade: TradeData = event.data
        if trade.id_ in self._trades:
            return
        self._trades[trade.id_] = trade
        strategy_ins = self.get_strategy_ins_by_oid(trade.order_id)
        self._datas[EventType.TRADE.name].append(trade)
        if strategy_ins is None:
            return
        strategy_ins.update_trade(trade)

    def get_event_datas(self, event_name):
        if event_name == EventType.ACCOUNT.name:
            return self._account.values()
        elif event_name == EventType.CONTRACT.name:
            return self._contract.values()
        elif event_name == EventType.POSITION.name:
            return self._positions.values()
        else:
            return self._datas.get(event_name, [])

    def on_position(self, event: Event):
        pos: PositionData = event.data
        self._positions[pos.id_] = pos

    def get_broker(self, name):
        return self.broker_instance.get(name)

    def get_order(self, order_id) -> OrderData:
        return self._orders.get(order_id)

    def get_tick(self, cid) -> TickData:
        """
        获取最新价格
        :param cid: code.exchange.contract_type
        :return: TickData
        """
        return self._ticks.get(cid)

    def get_position(self, pid) -> PositionData:
        """
        获取单个持仓
        :param pid: code.exchange.contract_type.direction
        :return: PositionData
        """
        return self._positions.get(pid)

    def get_all_positions(self) -> List[PositionData]:
        """
        获取持仓列表
        :return: List[PositionData]
        """
        return list(self._positions.values())

    def send_order(self, req: OrderRequest):
        broker = self.get_broker(req.broker_name)
        if broker is None:
            self.write_log(f'broker {req.broker_name} 不存在')
            return
        order_list = broker.send_order(req)
        for order in order_list:
            self._order_id_strategy_ins[order.t_order_id] = order.strategy_ins_name

    def write_log(self, msg):
        self.logger.info(msg)
        self.event_bus_left.put_log(msg)

    def join(self):
        self.guard_left.join()

    def tear_down(self):
        self.write_log('执行tear_down')
        for app in self.app_instance[::-1]:
            app.tear_down()
        self.guard_left.tear_down()

    def set_up(self):
        self.write_log('执行set_up')
        for app in self.app_instance:
            app.set_up()
        self.guard_left.set_up()

    def on_quit(self):
        self.tear_down()
